const Canva = {
    Framerate: 30,
    Terrain: null,
    RainDrops: new Queue(),
    AniDelay: 16,
    Perspect: null,
    Loop: true,
};  // Basic Canva Config

const World = {
    Camera: null,
    Radius: 600,
    Detail: 24,
    Height: 600,
    MaxObj: 7,
    Gravity: 30,
    WindStr: null,
    WindDir: null,
};  // Basic World Config

const Materials = {
    Water: null,
    Bush: null,
    Moss: null,
    Grass: null,
    Soil: null,
    Sand: null,
    Stone: null,
    Rock: null,
    Mud: null,
    Lightning: null,
};  // Basic Materials

const Rain = {
    Radius: 1.6,
    MaxNum: 500,
    Revive: 0.84,
    Emiter: null,
    Groups: 8,
    Splash: 6,
    SubRad: 0.8,
    MaxSplashSpd: 0.8,
    Ambient: document.getElementById("rain_sound"),
    Thunder: document.getElementById("thunder"),
    LightRd: 200,
    Environ: null,
};  // Rain Drop Config

class MeshTerrain {
    constructor(radius, detail, height, maxObj) {

        this.radius = radius;
        this.detail = detail;
        this.height = height;
        this.maxObj = maxObj;
        this.patch = 2*radius/detail;
        this.meshAnchors = [];
        this.meshPatches = [];
        this.meshObjects = [];
        this.meshActions = new Queue();
        this.meshColours = [];
        this.lightnings = new Queue();

        // Generate Terrain Mesh Anchors
        for (let iteD = 0; iteD <= detail; iteD++) {
            for (let iteW = 0; iteW <= detail; iteW++) {
                const X = lerp(-radius, radius, iteW/detail);
                const Z = lerp(-radius, radius, iteD/detail);

                let Y = map(noise(0.26*X/this.patch, 0.32*Z/this.patch), 0, 1, -height/28, height/15);
                Y += map(noise(0.16*X/this.patch, 0.2*Z/this.patch), 0, 1, -height/16, height/27);
                if (Y > 0) Y *= map(Math.random(), 0, 1, 1.4, 1.8);
                else Y *= map(Math.random(), 0, 1, 1.0, 1.2);

                this.meshAnchors.push([X, Y, Z]);
            }   // Generate Mesh Anchors along X Axis
        }   // Generate Mesh Anchors along Z Axis

        // Generate Terrain Mesh Patches
        for (let iteD = 0; iteD < detail; iteD++) {
            for (let iteW = 0; iteW < detail; iteW++) {
                this.meshPatches.push([
                    this.meshAnchors[iteD*(detail + 1) + iteW],
                    this.meshAnchors[(iteD + 1)*(detail + 1) + iteW],
                    this.meshAnchors[iteD*(detail + 1) + iteW + 1],
                    this.meshAnchors[(iteD + 1)*(detail + 1) + iteW + 1],
                ]); // Preload Mesh Patches
            }   // Generate Mesh Patches along X Axis
        }   // Generate Mesh Patches along Z Axis

        const objectHeight = World.Height/30;
        for (let iteD = 0; iteD < detail; iteD++) {
            for (let iteW = 0; iteW < detail; iteW++) {
                const objCt = Math.round(Math.random()*maxObj);
                const patch = this.meshPatches[iteD*detail + iteW];
                const meshObject = [];
                for (let iteO = 0; iteO < objCt; iteO++) {
                    const X = lerp(-radius, radius, iteW/detail)
                            + map(Math.random(), 0, 1, 0.08*detail, 0.92*detail);
                    const Z = lerp(-radius, radius, iteD/detail)
                            + map(Math.random(), 0, 1, 0.08*detail, 0.92*detail);

                    const slope = (Z - patch[1][2])/(X - patch[1][0]);
                    const ktile = (slope >= -1) ? patch[3] : patch[0];

                    const dist1 = dist(X, Z, patch[1][0], patch[1][2]);
                    const dist2 = dist(X, Z, ktile[0], ktile[2]);
                    const dist3 = dist(X, Z, patch[2][0], patch[2][2]);
                    const array = [[dist1, patch[1][1]], [dist2, ktile[1]], [dist3, patch[2][1]]];

                    const pro = array.reduce((pro, item) => pro + item[0]*item[1], 0);
                    const sum = array.reduce((sum, item) => sum + item[0], 0);

                    const objHht = 0.6*objectHeight + 0.4*objectHeight*Math.random();
                    if (pro/sum < -4) meshObject.push([X, pro/sum, Z, (0.8 + Math.random()/5)*objHht]);
                }   // Generate a Single Mesh Object
                this.meshObjects.push(meshObject);

                const average = patch.reduce((sum, item) => sum + item[1], 0)/4;
                let patchColor; const variant = [
                    12*(Math.random() - 0.5),
                    10*(Math.random() - 0.5),
                    11*(Math.random() - 0.5)
                ];  // Add Color Viberation

                if (average < -24) patchColor = Materials.Bush;
                else if (average < -18) patchColor = Materials.Moss;
                else if (average < -10) patchColor = Materials.Grass;
                else if (average < -2 ) patchColor = Materials.Soil;
                else if (average <  10) patchColor = Materials.Sand;
                else if (average <  16) patchColor = Materials.Stone;
                else if (average <  24) patchColor = Materials.Rock;
                else patchColor = Materials.Mud;

                this.meshColours.push(color(
                    variant[0] + 256*patchColor._array[0],
                    variant[1] + 256*patchColor._array[1],
                    variant[2] + 256*patchColor._array[2]
                )); // Generate Color for Mesh Patches
            }   // Generate Mesh Objects along X Axis
        }   // Generate Mesh Objects along Z Axis

    }   // Construct Procedural Map

    render() {

        push(); // Preserve Current Coordination
        stroke(0); strokeWeight(0.1);
        for (let iteD = 0; iteD < this.detail; iteD++) {
            let shift = iteD*this.detail;
            for (let iteW = 0; iteW < this.detail; iteW++) {
                const tile = this.meshPatches[shift + iteW];
                fill(this.meshColours[shift + iteW]);
                quad(
                    tile[0][0], tile[0][1], tile[0][2],
                    tile[1][0], tile[1][1], tile[1][2],
                    tile[3][0], tile[3][1], tile[3][2],
                    tile[2][0], tile[2][1], tile[2][2],
                );  // Render Terrain Patch
            }   // Render Each Column
        }   // Render Mesh Terrain
        pop();  // Restore Original Coordination

        for (let ite = 0, length = this.meshActions.length;
            ite < length; ite++) {
            const animate = this.meshActions.dequeue();
            if (animate.active()) {
                animate.render();
                this.meshActions.enqueue(animate);
            }   // Render When Valid
        }   // Render Terrain Animation

        noStroke(); fill(Materials.Grass);
        for (let iteD = 0; iteD < this.detail; iteD++) {
            for (let iteW = 0; iteW < this.detail; iteW++) {
                const meshObject = this.meshObjects[iteD*this.detail + iteW];
                for (let iteO = 0; iteO < meshObject.length; iteO++) {
                    push(); // Preserve Current Coordination
                    translate(meshObject[iteO][0], meshObject[iteO][1] - meshObject[iteO][3]/1.8, meshObject[iteO][2]);
                    rotateZ(Math.PI); cone(1, meshObject[iteO][3]);
                    pop();  // Restore Original Coordination
                }   // Render a Single Mesh Object
            }   // Render Mesh Objects along X Axis
        }   // Render Mesh Objects along Z Axis

        for (let ite = 0, length = this.lightnings.length; ite < length; ite++) {
            const lighting = this.lightnings.dequeue();
            if (lighting.active()) {
                lighting.render(); this.lightnings.enqueue(lighting);
            }   // Render When Valid
        }   // Render Lightnings

    }   // Render Low Poly Terrain

    surface(wCor, dCor) {

        // Restrict the Range of Horizontal Coordination
        if (wCor < -this.radius) wCor = -this.radius;
        if (wCor >= this.radius) wCor =  this.radius - 1;
        if (dCor < -this.radius) dCor = -this.radius;
        if (dCor >= this.radius) dCor =  this.radius - 1;

        const iteW = Math.floor((wCor + this.radius)/this.patch);
        const iteD = Math.floor((dCor + this.radius)/this.patch);

        // Fetch Anchor Information to Caculate Surface Altitude
        const patch = this.meshPatches[iteD*this.detail + iteW];
        const slope = (dCor - patch[1][2])/(wCor - patch[1][0]);
        const ktile = (slope >= -1) ? patch[3] : patch[0];

        const dist1 = dist(wCor, dCor, patch[1][0], patch[1][2]);
        const dist2 = dist(wCor, dCor, ktile[0], ktile[2]);
        const dist3 = dist(wCor, dCor, patch[2][0], patch[2][2]);
        const array = [[dist1, patch[1][1]], [dist2, ktile[1]], [dist3, patch[2][1]]];

        const pro = array.reduce((pro, item) => pro + item[0]*item[1], 0);
        const sum = array.reduce((sum, item) => sum + item[0], 0);

        return [(pro/sum > 0) ? 0 : pro/sum, iteD, iteW];

    }   // Return the Height of the Surface

}   // Low Poly Terrain

class RainDrop {
    constructor() {
        // Randomize Generate the Initial Location and Color
        const W = map(Math.random(), 0, 1, -World.Radius, World.Radius);
        const D = map(Math.random(), 0, 1, -World.Radius, World.Radius);
        this.location = [W, -World.Height, D];

        colorMode(HSL, 360, 1, 1, 1);   // Setup Color Mode
        this.color = color(Math.round(Math.random()*360), 0.8, 0.8, 0.6 + Math.random()*0.4);
        colorMode(RGB, 255, 255, 255, 255); // Restore Color Mode
    }   // Construct Rain Droplets

    render() {
        push(); // Preserve Current Coordination
        translate(this.location[0], this.location[1], this.location[2]);
        fill(this.color); noStroke(); sphere(Rain.Radius);
        pop();  // Restore Original Coordination
    }   // Render Rain Droplets

    descend(windW, windD) {
        this.location[1] += World.Gravity;
        this.location[0] += windW;
        this.location[2] += windD;
        if (Math.abs(this.location[0]) > World.Radius)
            this.location[0] = -this.location[0];
        if (Math.abs(this.location[2]) > World.Radius)
            this.location[2] = -this.location[2];
    }   // Calculate Next Location
}   // Rain Droplets

class Ripple {
    constructor(wCor, dCor, radius, color) {
        this.location = [wCor, 0, dCor];
        this.radius = radius;
        this.lifetime = 0;
        this.color = color;
    }   // Construct Ripples

    render() {
        push(); // Preserve Current Coordination
        translate(this.location[0], this.location[1], this.location[2]);
        this.lifetime += 1; noStroke(); rotateX(PI/2); fill(this.color);
        for (let R = lerp(1, this.radius, this.lifetime/Canva.AniDelay);
            R > 0; R -= this.radius/2) {
            torus(R, 0.4, 18, 10);
        }   // Render a Series of Ripple around the Center
        pop();  // Restore Original Coordination
    }   // Render Ripples

    active() {
        return (this.lifetime !== Canva.AniDelay);
    }   // Return True When Active
}   // Ripples

class Splash {
    constructor(wCor, hCor, dCor, color) {
        this.location = [wCor, hCor, dCor];
        this.lifetime = 0;
        this.amount = Math.round(1 + Rain.Splash*Math.random());
        this.splashes = [];
        this.color = color;

        for (let count = 0; count < this.amount; count++) {
            // Randomly Decide the Direction of the Splashed Drop
            const W = Math.random() - 0.5; const D = Math.random() - 0.5;
            const initSpeed = Rain.MaxSplashSpd*Math.random();
            const vertSpeed = 8*Rain.MaxSplashSpd*Math.random();
            const splash = [
                [0, 0, 0],
                [W/(W*W + D*D), D/(W*W + D*D)],
                [initSpeed, vertSpeed],
            ];  // Generate Splash Information
            this.splashes.push(splash);
        }   // Generate Water Splashes
    }   // Construct Water Splash

    render() {
        push(); // Preserve Current Coordination
        translate(this.location[0], this.location[1], this.location[2]);
        this.lifetime += 1; noStroke(); fill(this.color);
        for (let count = 0; count < this.amount; count++) {
            push(); // Preserve Current Coordination
            const splash = this.splashes[count];
            translate(splash[0][0], splash[0][1], splash[0][2]); sphere(Rain.SubRad);
            splash[0][0] += splash[2][0]*splash[1][0];
            splash[0][1] += lerp(-0.6*splash[2][1], 0.7*splash[2][1], this.lifetime/Canva.AniDelay);
            splash[0][2] += splash[2][0]*splash[1][1];
            pop();  // Restore Original Coordination
        }   // Render Water Splash Effect
        pop();  // Restore Original Coordination
    }   // Render Water Splash

    active() {
        return (this.lifetime != Canva.AniDelay);
    }   // Return True When Active
}   // Water Splash

class Thunder {
    constructor() {
        this.lifetime = 0;
        this.strikes = [];
        this.weight = Math.random();

        if ((this.weight > 0.48) && (this.weight < 0.64)) {
            Rain.Thunder.volume = 0.5 + Math.random()/2;
            Rain.Thunder.play();
        }   // Thunder Roars

        if ((this.weight > 0.43) && (this.weight < 0.76)) {
            const W = map(Math.random(), 0, 1, -World.Radius/2, World.Radius/2);
            const D = map(Math.random(), 0, 1, -World.Radius/2, World.Radius/2);
            for (let growth = 0; growth < World.Height;
                growth += World.Height*Math.random()/6) {
                this.strikes.push([
                    W + Rain.LightRd*(Math.random() - 0.5),
                    growth,
                    D + Rain.LightRd*(Math.random() - 0.5)
                ]); // Generate Lightning Anchors
            }   // Generate Lightning Strike
        }   // Lightning Strikes

        this.weight *= 6; this.weight += 6;
    }   // Construct Thunder

    render() {
        noFill(); stroke(Materials.Lightning); strokeWeight(this.weight); beginShape();
        for (let ite = 0; ite < this.strikes.length; ite++) {
            vertex(this.strikes[ite][0], -this.strikes[ite][1], this.strikes[ite][2]);
        }   // Render All Strikes
        endShape(); this.lifetime += 2;
    }   // Render Thunder

    active() {
        return (this.lifetime < Canva.AniDelay);
    }   // Return True When Active

}   // Thunder

function setup() {
    // Setup the Canvas for Sketching
    createCanvas(innerWidth, innerHeight, WEBGL);

    // Setup Viewpoint for Camera
    World.Camera = createCamera();
    World.Camera.setPosition(1.4*World.Radius, -1.62*World.Radius, 1.4*World.Radius);
    World.Camera.lookAt(0, 0, 0);
    World.Camera.perspective(0.258);

    // Setup Environment Configurations
    angleMode(RADIANS); frameRate(Canva.Framerate);
    setTimeout(() => {
        Rain.Ambient.play();
    }, 2.4*World.Height/World.Gravity/Canva.Framerate);

    // Setup Color Configuration
    Materials.Bush = color(0x06, 0x3C, 0x06);
    Materials.Moss = color(0x13, 0x63, 0x03);
    Materials.Grass = color(0x1B, 0x55, 0x1B);
    Materials.Soil = color(0x62, 0x40, 0x32);
    Materials.Sand = color(0x9C, 0x95, 0x81);
    Materials.Stone = color(0x5C, 0x5B, 0x4D);
    Materials.Rock = color(0x2B, 0x24, 0x12);
    Materials.Mud = color(0x1C, 0x13, 0x03);
    Materials.Water = color(0x69, 0xB6, 0xDD, 148);
    Materials.Lightning = color(0xAE, 0xA0, 0xED);

    Rain.Emiter = document.getElementById("rainfall");
    World.WindDir = document.getElementById("windDir");
    World.WindStr = document.getElementById("windStr");
    Canva.Perspect = document.getElementById("cameraVw");
    Rain.Environ = document.getElementById("ambient");

    Canva.Terrain = new MeshTerrain(
        World.Radius,
        World.Detail,
        World.Height,
        World.MaxObj,
    );  // Generate Terrain for the World
}   // Setup the Environment

function draw() {
    rotateY(Number(Canva.Perspect.value));  // Rotate Camera

    const WindW = Number(World.WindStr.value)*Math.cos(Number(World.WindDir.value));
    const WindD = Number(World.WindStr.value)*Math.sin(Number(World.WindDir.value));
    const length = Canva.RainDrops.length;

    Canva.Terrain.render(); noStroke(); fill(Materials.Water);
    rotateX(PI/2); plane(2*World.Radius); rotateX(-PI/2);


    for (let count = 0; count < length; count++) {
        const drop = Canva.RainDrops.dequeue();
        const surf = Canva.Terrain.surface(drop.location[0], drop.location[2]);
        if (drop.location[1] > surf[0]) {
            // Trigger Terrain Animation
            const animate = (surf[0] < 0) ? new Splash(drop.location[0], surf[0], drop.location[2], drop.color)
                                          : new Ripple(drop.location[0], drop.location[2], 8, drop.color);
            Canva.Terrain.meshActions.enqueue(animate);

            // Update Rain Drop Stat
            if ((Canva.RainDrops.length < Rain.MaxNum)
             && (Math.random() < Rain.Revive)) {
                Canva.RainDrops.enqueue(new RainDrop());
            }   // Generate Rain Drops
        }   // Remove Rain Drop and Trigger Animation
        else {
            const shiftW = Number(World.WindStr.value)*(Math.random() - 0.5)/3;
            const shiftD = Number(World.WindStr.value)*(Math.random() - 0.5)/4;
            drop.render(); drop.descend(WindW + shiftW, WindD + shiftD);
            Canva.RainDrops.enqueue(drop);
        }   // Render and Generate Next Location
    }   // Render Rain Drops for the World

    let eventTrigger = Math.random();

    if (length > Rain.MaxNum*0.54) {
        Rain.Ambient.volume = 1.0;
        if (eventTrigger < 0.04) Canva.Terrain.lightnings.enqueue(new Thunder());
    }   // Thunder Roars
    else Rain.Ambient.volume = 1.62*length/Rain.MaxNum;

    if ((Canva.RainDrops.length < Rain.MaxNum)
     && (eventTrigger < 0.5*Number(Rain.Emiter.value))) {
        for (let count = 0, num = Math.random()*Rain.Groups; count < num; count++)
            Canva.RainDrops.enqueue(new RainDrop());
    }   // Generate Rain Drops for the World

    Rain.Environ.style.opacity = Math.fround(0.56*length/Rain.MaxNum);
}   // Render the Scenario

function keyPressed() {
    if (keyCode === 0x20) {
        Canva.Loop ? noLoop() : loop();
        Canva.Loop = !Canva.Loop;
    }   // Loop or Pause
}   // Trigger Interuption
